iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Rust

Rust 實戰專案集:30 個漸進式專案從工具到服務系列 第 6

檔案加密工具 - 使用 AES 加密保護重要檔案

  • 分享至 

  • xImage
  •  

前言

今天我們要用做加密檔案的工具,以 AES 加密方式加密檔案。

為什麼選擇 AES 加密?

主要是 AES 加密算是一個比較廣泛的技術,所以用 AES 加密基本上妥妥的,沒問題

那 AES 加密有幾個基本優點

  • 安全性高:經過多年的密碼學分析,至今沒有發現有效的攻擊方法
  • 效率佳:在軟體和硬體實現上都有很好的性能
  • 標準化:被 NIST(美國國家標準技術研究所)採用為官方標準
  • 廣泛支援:幾乎所有的加密庫都支援 AES

補充:AES 的全名為 Advanced Encryption Standard

讓我們開始吧

先起個專案,老樣子

cargo new file_encryptor
cd file_encryptor

以下是我們會用到的依賴

[dependencies]
aes = "0.8"
cbc = "0.1"
sha2 = "0.10"
pbkdf2 = "0.12"
rand = "0.8"
clap = { version = "4.0", features = ["derive"] }
hex = "0.4"
anyhow = "1.0"

這時候我們建立 src/crypto.rs 製作我們的核心加密組件

// src/crypto.rs
use aes::Aes256;
use cbc::{Decryptor, Encryptor};
use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use pbkdf2::pbkdf2_hmac;
use rand::{RngCore, rngs::OsRng};
use sha2::Sha256;
use anyhow::{Result, Context};

type Aes256CbcEnc = Encryptor<Aes256>;
type Aes256CbcDec = Decryptor<Aes256>;

const KEY_LENGTH: usize = 32; // AES-256
const IV_LENGTH: usize = 16;  // AES block size
const SALT_LENGTH: usize = 32;
const PBKDF2_ITERATIONS: u32 = 100_000;

pub struct FileEncryption {
    key: [u8; KEY_LENGTH],
}

impl FileEncryption {
    /// 從密碼派生金鑰
    pub fn from_password(password: &str, salt: &[u8]) -> Result<Self> {
        if salt.len() != SALT_LENGTH {
            anyhow::bail!("Salt must be {} bytes long", SALT_LENGTH);
        }

        let mut key = [0u8; KEY_LENGTH];
        pbkdf2_hmac::<Sha256>(
            password.as_bytes(),
            salt,
            PBKDF2_ITERATIONS,
            &mut key,
        );

        Ok(Self { key })
    }

    /// 生成隨機鹽值
    pub fn generate_salt() -> [u8; SALT_LENGTH] {
        let mut salt = [0u8; SALT_LENGTH];
        OsRng.fill_bytes(&mut salt);
        salt
    }

    /// 加密資料
    pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
        // 生成隨機 IV
        let mut iv = [0u8; IV_LENGTH];
        OsRng.fill_bytes(&mut iv);

        // 為 PKCS7 padding 準備緩衝區
        let mut buffer = data.to_vec();
        let padding_len = 16 - (data.len() % 16);
        buffer.extend(vec![padding_len as u8; padding_len]);

        // 執行加密
        let cipher = Aes256CbcEnc::new(&self.key.into(), &iv.into());
        let encrypted = cipher.encrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(&buffer)
            .context("Encryption failed")?;

        // 將 IV 和加密資料組合
        let mut result = Vec::with_capacity(IV_LENGTH + encrypted.len());
        result.extend_from_slice(&iv);
        result.extend_from_slice(&encrypted);

        Ok(result)
    }

    /// 解密資料
    pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>> {
        if encrypted_data.len() < IV_LENGTH {
            anyhow::bail!("Encrypted data too short");
        }

        // 分離 IV 和加密資料
        let (iv, encrypted) = encrypted_data.split_at(IV_LENGTH);
        
        // 執行解密
        let cipher = Aes256CbcDec::new(self.key.as_slice().into(), iv.into());
        let mut decrypted = encrypted.to_vec();
        let decrypted_data = cipher.decrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(&mut decrypted)
            .context("Decryption failed")?;

        // 移除 PKCS7 padding
        let padding_len = *decrypted_data.last().unwrap() as usize;
        if padding_len > 16 || padding_len == 0 {
            anyhow::bail!("Invalid padding");
        }

        let data_len = decrypted_data.len() - padding_len;
        Ok(decrypted_data[..data_len].to_vec())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_encryption_decryption() {
        let password = "test_password_123";
        let salt = FileEncryption::generate_salt();
        let encryption = FileEncryption::from_password(password, &salt).unwrap();

        let original_data = b"Hello, this is a test message for encryption!";
        let encrypted = encryption.encrypt(original_data).unwrap();
        let decrypted = encryption.decrypt(&encrypted).unwrap();

        assert_eq!(original_data, decrypted.as_slice());
    }

    #[test]
    fn test_different_passwords_different_keys() {
        let salt = FileEncryption::generate_salt();
        let enc1 = FileEncryption::from_password("password1", &salt).unwrap();
        let enc2 = FileEncryption::from_password("password2", &salt).unwrap();

        assert_ne!(enc1.key, enc2.key);
    }
}

開始做檔案操作相關的組件

// src/file_ops.rs
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use anyhow::{Result, Context};
use crate::crypto::{FileEncryption, SALT_LENGTH};

const MAGIC_HEADER: &[u8] = b"RUSTENC1"; // 8 bytes magic header
const HEADER_SIZE: usize = MAGIC_HEADER.len() + SALT_LENGTH; // 8 + 32 = 40 bytes

pub fn encrypt_file<P: AsRef<Path>>(
    input_path: P,
    output_path: P,
    password: &str,
) -> Result<()> {
    // 讀取原始檔案
    let mut input_file = File::open(&input_path)
        .with_context(|| format!("Failed to open input file: {:?}", input_path.as_ref()))?;
    
    let mut data = Vec::new();
    input_file.read_to_end(&mut data)
        .context("Failed to read input file")?;

    // 生成鹽值並建立加密器
    let salt = FileEncryption::generate_salt();
    let encryption = FileEncryption::from_password(password, &salt)?;

    // 加密資料
    let encrypted_data = encryption.encrypt(&data)
        .context("Failed to encrypt data")?;

    // 寫入加密檔案
    let mut output_file = File::create(&output_path)
        .with_context(|| format!("Failed to create output file: {:?}", output_path.as_ref()))?;

    // 寫入標頭(magic header + salt)
    output_file.write_all(MAGIC_HEADER)
        .context("Failed to write magic header")?;
    output_file.write_all(&salt)
        .context("Failed to write salt")?;
    
    // 寫入加密資料
    output_file.write_all(&encrypted_data)
        .context("Failed to write encrypted data")?;

    println!("File encrypted successfully: {:?} -> {:?}", 
             input_path.as_ref(), output_path.as_ref());
    
    Ok(())
}

pub fn decrypt_file<P: AsRef<Path>>(
    input_path: P,
    output_path: P,
    password: &str,
) -> Result<()> {
    // 讀取加密檔案
    let mut input_file = File::open(&input_path)
        .with_context(|| format!("Failed to open encrypted file: {:?}", input_path.as_ref()))?;

    let mut encrypted_content = Vec::new();
    input_file.read_to_end(&mut encrypted_content)
        .context("Failed to read encrypted file")?;

    // 檢查檔案大小
    if encrypted_content.len() < HEADER_SIZE {
        anyhow::bail!("File too small to be a valid encrypted file");
    }

    // 檢查 magic header
    if &encrypted_content[..MAGIC_HEADER.len()] != MAGIC_HEADER {
        anyhow::bail!("Invalid file format or not an encrypted file");
    }

    // 提取鹽值
    let salt = &encrypted_content[MAGIC_HEADER.len()..HEADER_SIZE];
    
    // 提取加密資料
    let encrypted_data = &encrypted_content[HEADER_SIZE..];

    // 建立解密器
    let encryption = FileEncryption::from_password(password, salt)?;

    // 解密資料
    let decrypted_data = encryption.decrypt(encrypted_data)
        .context("Failed to decrypt data - wrong password or corrupted file")?;

    // 寫入解密檔案
    let mut output_file = File::create(&output_path)
        .with_context(|| format!("Failed to create output file: {:?}", output_path.as_ref()))?;

    output_file.write_all(&decrypted_data)
        .context("Failed to write decrypted data")?;

    println!("File decrypted successfully: {:?} -> {:?}", 
             input_path.as_ref(), output_path.as_ref());

    Ok(())
}

實作 main.rs

// src/main.rs
mod crypto;
mod file_ops;

use clap::{Parser, Subcommand};
use anyhow::Result;
use std::io::{self, Write};

#[derive(Parser)]
#[command(name = "file-encryptor")]
#[command(about = "A secure file encryption tool using AES-256")]
#[command(version = "1.0")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Encrypt a file
    Encrypt {
        /// Input file path
        #[arg(short, long)]
        input: String,
        
        /// Output file path
        #[arg(short, long)]
        output: String,
    },
    /// Decrypt a file
    Decrypt {
        /// Input encrypted file path
        #[arg(short, long)]
        input: String,
        
        /// Output file path
        #[arg(short, long)]
        output: String,
    },
}

fn get_password(prompt: &str) -> Result<String> {
    print!("{}", prompt);
    io::stdout().flush()?;
    
    // 注意:在實際應用中,應該使用不會回顯密碼的輸入方式
    // 這裡為了簡單示範使用標準輸入
    let mut password = String::new();
    io::stdin().read_line(&mut password)?;
    
    Ok(password.trim().to_string())
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Encrypt { input, output } => {
            let password = get_password("Enter password for encryption: ")?;
            
            if password.is_empty() {
                anyhow::bail!("Password cannot be empty");
            }

            file_ops::encrypt_file(&input, &output, &password)?;
        }
        Commands::Decrypt { input, output } => {
            let password = get_password("Enter password for decryption: ")?;
            
            if password.is_empty() {
                anyhow::bail!("Password cannot be empty");
            }

            file_ops::decrypt_file(&input, &output, &password)?;
        }
    }

    Ok(())
}

開始使用 try try

# 編譯專案
cargo build --release

# 加密檔案
./target/release/file-encryptor encrypt -i secret.txt -o secret.txt.enc

# 解密檔案
./target/release/file-encryptor decrypt -i secret.txt.enc -o decrypted.txt

打完收工~讚!


上一篇
密碼產生器 - 可自訂規則的安全密碼產生工具
系列文
Rust 實戰專案集:30 個漸進式專案從工具到服務6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言